package mx.vlabs.soapdroid.dto;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Vector;

import javax.management.RuntimeErrorException;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.XMLGregorianCalendar;

import mx.vlabs.soapdroid.annotations.XmlElement;
import mx.vlabs.soapdroid.annotations.XmlSchema;
import mx.vlabs.soapdroid.annotations.XmlType;
import mx.vlabs.soapdroid.utils.Utils;

import org.ksoap2.serialization.PropertyInfo;
import org.ksoap2.serialization.SoapObject;
import org.ksoap2.serialization.SoapPrimitive;

public class DTO extends SoapObject{
	
	private Map<String,Method> setters;
	private Map<String,Method> getters;
	private Field[]  fields;
	protected String[] fieldNames;
	protected String[] propertyNames;
	
	/**
	 * 
	 */
	public DTO(){
		super();
		configureName();
		init();
	}
	
	/**
	 * @param namespace
	 * @param name
	 */
	public DTO(String namespace,String name){
		super(namespace, name);
		init();
	}
	
	
	/**
	 * 
	 */
	private void init(){
		
		setters = new HashMap<String, Method>();
		getters = new HashMap<String, Method>();
		
		loadFieldList();
		
		loadSettersAndGetters(fields);
		
	}
	
	/**
	 * 
	 */
	private void configureName(){
		
		Class<?> c = this.getClass();
		
		XmlType xmlType = c.getAnnotation(XmlType.class);
		
		XmlSchema xmlSchema = c.getPackage().getAnnotation(XmlSchema.class);
		
		if(xmlSchema!=null){
	    	namespace = xmlSchema.namespace();
		}
		
		if(xmlType!=null){
			
			name = xmlType.name().equals("") ? 
				   Utils.uncapitalizeFirstLetter(this.getClass().getSimpleName()) :
				   xmlType.name();
		 
		    if(namespace==null||namespace.equals(""))
			namespace = xmlType.namespace();

		}else{
			
			name = Utils.uncapitalizeFirstLetter(this.getClass().getSimpleName());
		}
		
	}
	
	
	
	/**
	 * 
	 */
	private void loadFieldList(){
		
		Class<?> c = this.getClass();
		
		
		XmlType xmlType = c.getAnnotation(XmlType.class);
		
		
		if((xmlType==null)||
		    (	
				(xmlType!=null) &&
				(
					((xmlType.propOrder().length==1) &&	(xmlType.propOrder()[0].equals("")))||
					(xmlType.propOrder().length==0)
				)
			)
		){
			fields = c.getDeclaredFields();
		}else{
			
			fields = new Field[xmlType.propOrder().length];
			
			for(int i=0;i<fields.length;i++){
				

				try {
					fields[i] = c.getDeclaredField(xmlType.propOrder()[i]);
				} catch (NoSuchFieldException e) {
					// TODO Auto-generated catch block
					Field tempField = null;
					
					for(Field field: c.getDeclaredFields()){
						
						XmlElement xmlElement = field.getAnnotation(XmlElement.class);
						if(xmlElement!=null){
							
							if(xmlElement.name().equals(xmlType.propOrder()[i])){
								tempField = field;
								break;
							}
							
						}
						
					}
					
					if(tempField!=null){
						fields[i] = tempField;
					}else{
						e.printStackTrace();
					}
				} catch (SecurityException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
			
		}
		
	}
	
	/**
	 * @param fields
	 */
	private void loadSettersAndGetters(Field[] fields){
		
		fieldNames = new String[fields.length];
		
		int i = 0;
		
		for(Field field:fields){
			
			
			
			try {
				//if(!field.getType().equals(List.class))
				fieldNames[i] = getFieldName(field);
				loadSetter(field);
				loadGetter(field);
				
				i++;
			} catch (Exception e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		
	}
	
	public String[] getPropertyNames() {
		return propertyNames;
	}

	/**
	 * @param field
	 * @return
	 */
	private String getFieldName(Field field){
		
		XmlElement xmlElement = field.getAnnotation(XmlElement.class);
		
		if(xmlElement!=null&&!xmlElement.name().equals(""))
			return xmlElement.name();
		else
			return field.getName();
		
	}
	
	public Field getFieldByName(String propName){

		for(Field field : fields){
			
			if(getFieldName(field).equals(propName)){
				return field;
			}

		}
		return null;
		
	}
	
	public int getPropertyIndexByName(String propName){
		
		int index = -1;
		int i = 0;
		
		for(Field field : fields){
			
			if(getFieldName(field).equals(propName)){
				index = i;
				break;
			}
			
			i++;
		}
		
		return index;
		
	}
	
	/**
	 * @param field
	 * @throws NoSuchMethodException
	 * @throws SecurityException
	 */
	private void loadSetter(Field field) throws NoSuchMethodException, SecurityException{
		
		final String SET_PREFIX = "set";
		
		String fieldName = getFieldName(field);
		
		String methodName = SET_PREFIX+Utils.capitalizeFirstLetter(fieldName);
		Method setter = this.getClass().getDeclaredMethod(methodName, field.getType());
		setters.put(fieldName, setter);
	}
	
	
	/**
	 * @param field
	 * @throws NoSuchMethodException
	 * @throws SecurityException
	 */
	private void loadGetter(Field field) throws NoSuchMethodException, SecurityException{
		
		final String GET_PREFIX = getFieldType(field.getType())
					.equals(Boolean.class) ? "is" : "get";
		
		String fieldName = getFieldName(field);
		
		String methodName = GET_PREFIX+Utils.capitalizeFirstLetter(fieldName);
		Method getter = this.getClass().getDeclaredMethod(methodName);
		getters.put(fieldName, getter);
		
	}
	
	private Class getFieldType(Type type){
		
		if(type.equals(int.class)||type.equals(Integer.class)){
			return Integer.class;
		}else if(type.equals(float.class)||type.equals(Float.class)){
			return Float.class;
		}else if(type.equals(double.class)||type.equals(Double.class)){
			return Double.class;
		}else if(type.equals(long.class)||type.equals(Long.class)){
			return Long.class;
		}else if(type.equals(boolean.class)||type.equals(Boolean.class)){
			return Boolean.class;
		}else if(type.equals(String.class)){
			return String.class;
		}else{
			//return PropertyInfo.OBJECT_CLASS;
			return (Class) type;
		}
	}
	
	@SuppressWarnings("unchecked")
	public static <DTOType extends DTO> DTOType load(SoapObject soapObject,Class<DTOType> dtoTypeClass) throws Exception{
		
		
		//DTOType dto = dtoTypeClass.getConstructor(String.class,String.class).newInstance(soapObject.getNamespace(),soapObject.getName());
		DTOType dto = dtoTypeClass.newInstance();
		
		

		PropertyInfo info = new PropertyInfo();
		
		dto.propertyNames = new String[soapObject.getPropertyCount()];
		
		for (int i = 0; i < soapObject.getPropertyCount(); i++) {
			 soapObject.getPropertyInfo(i, info); 
			 dto.propertyNames[i] = info.name;
			 dto.setDTOProperty(info.name, info.getValue());
		}
			
		return dto;
		
	}
	
	public DTO explode(){
		
		this.properties = new Vector();
		
		for(String propName : fieldNames){
			
			Object value = getDTOProperty(propName);
			
			if(value instanceof DTO){
				DTO dtoTemp = (DTO) value;
				value = dtoTemp.explode();
				writeSOProperty(propName, value);
			}else if(value instanceof List){
				
				List tempList = (List) value;
				
				if(Utils.isDTOListType(getFieldByName(propName))){
					
					for(Object item : tempList){
						
						DTO dtoItem = (DTO) item;
						dtoItem.explode();
						writeSOProperty(propName, dtoItem);
						
					}
					
				}else{
					
					for(Object item : tempList){
						writeSOProperty(propName, item);
					}
					
				}
				
			}else if(value.getClass().isEnum()){
				
				writeSOProperty(propName, value.toString());
				
			}else{
				
				writeSOProperty(propName, value);
			}
			
		}
	
		return this;
	}
	
	public Object getDTOProperty(String propName){
		
		try {
			return getters.get(propName).invoke(this);
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			return null;
		}
		
	}
	
	public void setDTOProperty(String propName,Object value){
		
		int propIndex = getPropertyIndexByName(propName);
		
		value = readSOValue(getFieldByName(propName), value);
		
		try {
			
			if(setters.get(propName).getModifiers() != Modifier.PUBLIC){
				setters.get(propName).setAccessible(true);
			}
			setters.get(propName)
			.invoke(this, getFieldType(fields[propIndex].getType()).cast(value));
			
			if(setters.get(propName).getModifiers() != Modifier.PUBLIC){
				setters.get(propName).setAccessible(false);
			}
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		
	}
	
	@SuppressWarnings({ "unchecked", "rawtypes" })
	private Object readSOValue(Field field,Object arg1){
		
		Type type = field.getType();
		
		if((arg1 instanceof SoapPrimitive)&&!getFieldType(type).equals(List.class)){
			
			if(getFieldType(type).equals(Boolean.class)){
				arg1 = Boolean.parseBoolean(arg1.toString());
			}else if(getFieldType(type).equals(Integer.class)){
				arg1 = Integer.parseInt(arg1.toString());
				
			}else if(getFieldType(type).equals(Long.class)){
				arg1 = Long.parseLong(arg1.toString());
			}else if(getFieldType(type).equals(Double.class)){
				arg1 = Double.parseDouble(arg1.toString());
			}else if(getFieldType(type).equals(Float.class)){
				arg1 = Float.parseFloat(arg1.toString());
			}else if(getFieldType(type).equals(Date.class)){
				
				XMLGregorianCalendar date = null;
				try {
					date = DatatypeFactory.newInstance().newXMLGregorianCalendar(arg1.toString());
					
					
					arg1 = (Date) new Date(date.toGregorianCalendar().getTimeInMillis());
					
				} catch (Exception e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				
			
			}else{
				arg1 = ((SoapPrimitive) arg1).getValue();
			}
		}else if(getFieldType(type).equals(List.class)){
			
			
			Object listTempVal = getDTOProperty(getFieldName(field));
			
			List list = null;
			
			if(listTempVal instanceof List){
				list = (List) listTempVal;
			}else{
				list = new ArrayList();
			}
			
			if(Utils.isDTOType((Class<?>) Utils.getListType(field))){
				
				try {
					list.add(DTO.load((SoapObject) arg1,(Class<DTO>) Utils.getListType(field)));
				} catch (Exception e) {
					e.printStackTrace();
				} 
			
				
				
			}else{
				list.add(arg1);
			}
			
		
			arg1 = list;
			
		}else if(arg1 instanceof SoapObject){
			
			try {
				arg1 = DTO.load((SoapObject) arg1, getFieldType(type));
			} catch (Exception e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			
		}
		
		return arg1;
		
	}
	
	private void writeSOProperty(String propName,Object value){
		
		Field field = getFieldByName(propName);
		
		Type type = field.getType();
		
		if(value!=null){
		
			if(getFieldType(type).equals(Date.class)){
				
				Date date = (Date) value;
				
				GregorianCalendar gc = new GregorianCalendar();
			    gc.setTimeInMillis(date.getTime());
				try {
					XMLGregorianCalendar xmlGC = DatatypeFactory.newInstance().newXMLGregorianCalendar(gc);
		
					value = xmlGC.toXMLFormat();
					
					this.addProperty(propName,value);
					
				} catch (Exception e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}else if(value instanceof List){
				
				List tempList = (List) value;
				
				for(Object item: tempList){
					this.addProperty(getFieldName(field), value);
				}
				
		
			}else{
				
				this.addProperty(propName,value);
				
			}
		
		}
	}

	public String[] getFieldNames() {
		return fieldNames;
	}
	
	
	
}
